<!DOCTYPE html>
<!--
Pyodide Console v2 (Experimental)

This is an experimental version of the Pyodide console that provides
an enhanced terminal experience using xterm.js. This implementation replaces
the jQuery Terminal used in the original console with a more feature-rich
terminal emulator that offers better performance and modern terminal capabilities.

Note: This console is still under development and may not have all the features
of the stable console.
 -->
<html>
  <head>
    <title>Pyodide Console</title>
    <meta charset="UTF-8" />
    <meta
      http-equiv="origin-trial"
      content="Aq6vv/4syIkcyMszFgCc9LlH0kX88jdE7SXfCFnh2RQN0nhhL8o6PCQ2oE3a7n3mC7+d9n89Repw5HYBtjarDw4AAAB3eyJvcmlnaW4iOiJodHRwczovL3B5b2RpZGUub3JnOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseUpTUHJvbWlzZUludGVncmF0aW9uIiwiZXhwaXJ5IjoxNzMwMjQ2Mzk5LCJpc1N1YmRvbWFpbiI6dHJ1ZX0="
    />
    <meta
      http-equiv="origin-trial"
      content="Ai8IXb0XqedlM/Q2guWXFfBkKiYY9uaPZpdjHqc8y0ZvpAfK9SKzp/dIuFH+txG/HEKxt59uIkk39hhWrhNgbw4AAABieyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwMDAiLCJmZWF0dXJlIjoiV2ViQXNzZW1ibHlKU1Byb21pc2VJbnRlZ3JhdGlvbiIsImV4cGlyeSI6MTczMDI0NjM5OX0="
    />
    <link
      rel="stylesheet"
      href="https://unpkg.com/@xterm/xterm@5.4.0/css/xterm.css"
    />
    <link
      href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐍</text></svg>"
      rel="icon"
    />
    <script src="https://unpkg.com/@xterm/xterm@5.4.0/lib/xterm.js"></script>
    <script src="https://unpkg.com/@xterm/addon-fit@0.9.0/lib/addon-fit.js"></script>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }

      html,
      body {
        margin: 0;
        background-color: #000000;
        font-family: "Monaco", "Menlo", "Courier New", monospace;
        overflow: hidden;
      }

      #terminal {
        position: fixed;
        inset: 10px;
      }

      #loading {
        display: inline-block;
        width: 50px;
        height: 50px;
        position: fixed;
        top: 50%;
        left: 50%;
        border: 3px solid rgba(172, 237, 255, 0.5);
        border-radius: 50%;
        border-top-color: #fff;
        animation: spin 1s ease-in-out infinite;
        -webkit-animation: spin 1s ease-in-out infinite;
      }

      @keyframes spin {
        to {
          -webkit-transform: rotate(360deg);
        }
      }
      @-webkit-keyframes spin {
        to {
          -webkit-transform: rotate(360deg);
        }
      }
    </style>
  </head>
  <body>
    <div id="loading"></div>
    <div id="terminal"></div>
    <script type="module">
      async function main() {
        const fitAddon = new FitAddon.FitAddon();
        const term = new Terminal({
          cursorBlink: true,
          cursorStyle: "block",
          convertEol: true,
          scrollback: 2_000,
          fontSize: 18,
          lineHeight: 1.4,
          fontFamily: "monospace",
          theme: {
            background: "#000000",
            foreground: "rgba(255, 255, 255, 0.8)",
            cursor: "rgba(255, 255, 255, 0.8)",
            selection: "#404040",
            error: "#ff0000",
          },
        });
        window.term = term;

        term.open(document.getElementById("terminal"));
        term.loadAddon(fitAddon);

        fitAddon.fit();
        term.focus();

        window.addEventListener("resize", () => {
          setTimeout(() => fitAddon.fit(), 50);
        });

        // Re-fit after the page has fully loaded
        window.addEventListener("load", () => {
          setTimeout(() => fitAddon.fit(), 100);
        });

        // Initialize Pyodide
        let indexURL = "{{ PYODIDE_BASE_URL }}";
        const urlParams = new URLSearchParams(window.location.search);
        const buildParam = urlParams.get("build");
        if (buildParam && ["full", "debug", "pyc"].includes(buildParam)) {
          indexURL = indexURL.replace("/full/", "/" + buildParam + "/");
        }

        const { loadPyodide } = await import(indexURL + "pyodide.mjs");
        const pyodide = await loadPyodide();
        globalThis.pyodide = pyodide;

        // Hide loading spinner
        document.getElementById("loading").style.display = "none";

        const { repr_shorten, BANNER, PyodideConsole } =
          pyodide.pyimport("pyodide.console");

        term.writeln(
          `Welcome to the Pyodide ${pyodide.version} terminal emulator 🐍\n${BANNER}`
        );

        const pyconsole = PyodideConsole(pyodide.globals);

        const namespace = pyodide.globals.get("dict")();
        const await_fut = pyodide.runPython(
          `
      import builtins
      from pyodide.ffi import to_js
      async def await_fut(fut):
        res = await fut
        if res is not None:
            builtins._ = res
        return to_js([res], depth=1)
      await_fut
      `,
          { globals: namespace }
        );
        namespace.destroy();

        pyconsole.stdout_callback = (s) => term.write(s);
        pyconsole.stderr_callback = (s) => term.write(`\x1b[31m${s}\x1b[0m`);

        // Handle fatal errors
        pyodide._api.on_fatal = async (e) => {
          if (e.name === "Exit") {
            term.write(`\x1b[31m${e}\x1b[0m\r\n`);
            term.write(
              "\x1b[31mPyodide exited and can no longer be used.\x1b[0m\r\n"
            );
          } else {
            term.write(
              "\x1b[31mPyodide has suffered a fatal error. Please report this to the Pyodide maintainers.\x1b[0m\r\n"
            );
            term.write("\x1b[31mThe cause of the fatal error was:\x1b[0m\r\n");
            term.write(`\x1b[31m${e.message || e}\x1b[0m\r\n`);
            term.write(
              "\x1b[31mLook in the browser console for more details.\x1b[0m\r\n"
            );
          }
        };

        // REPL implementation
        const ps1 = ">>> ";
        const ps2 = "... ";
        let buffer = "";
        let cursorIndex = 0; // index within buffer for in-line editing
        let prompt = ps1;
        const history = [];
        let historyIndex = null; // null means not navigating history

        // Load history from localStorage
        try {
          const savedHistory = localStorage.getItem("0_commands");
          if (savedHistory) {
            const parsed = JSON.parse(savedHistory);
            if (Array.isArray(parsed)) {
              history.push(...parsed);
            }
          }
        } catch (e) {
          console.error("Failed to load history from localStorage:", e);
        }

        term.write(prompt);

        function addToHistory(command) {
          const trimmed = command.trimEnd();
          if (!trimmed) return;
          const last = history[history.length - 1];
          if (last !== trimmed) {
            history.push(trimmed);
            localStorage.setItem("0_commands", JSON.stringify(history));
          }
        }

        function refreshLine() {
          // Write left part, save cursor, write right part, clear, restore cursor.
          const clearCommand = "\x1b[0K";
          const leftPart = prompt + buffer.slice(0, cursorIndex);
          const rightPart = buffer.slice(cursorIndex);
          term.write(
            `\x1b[0G${leftPart}\x1b[s${rightPart}${clearCommand}\x1b[u`
          );
        }

        function setBuffer(newBuffer, newCursorIndex = null) {
          buffer = newBuffer;
          if (newCursorIndex === null) {
            cursorIndex = buffer.length;
          } else {
            cursorIndex = Math.max(0, Math.min(newCursorIndex, buffer.length));
          }
          refreshLine();
        }

        async function execLine(line) {
          // Normalize non-breaking spaces to regular spaces
          line = line.replace(/\u00a0/g, " ");
          // clear the terminal
          if (line === "clear") {
            term.clear();
            return;
          }

          const fut = pyconsole.push(line);

          switch (fut.syntax_check) {
            case "syntax-error":
              term.write(`\x1b[31m${fut.formatted_error.trimEnd()}\x1b[0m`);
              term.write("\r\n");
              prompt = ps1;
              addToHistory(line);
              historyIndex = null;
              fut.destroy();
              break;
            case "incomplete":
              prompt = ps2;
              addToHistory(line);
              historyIndex = null;
              return;
            case "complete":
              prompt = ps1;
              try {
                const wrapped = await_fut(fut);
                const [value] = await wrapped;
                if (value !== undefined) {
                  const output = repr_shorten.callKwargs(value, {
                    separator: "\n<long output truncated>\n",
                  });
                  term.write(output);
                  term.write("\r\n");
                }
                if (value instanceof pyodide.ffi.PyProxy) value.destroy();
                wrapped.destroy();
              } catch (e) {
                const msg = fut.formatted_error || e.message;
                term.write(`\x1b[31m${String(msg).trimEnd()}\x1b[0m`);
                term.write("\r\n");
              } finally {
                fut.destroy();
              }
              addToHistory(line);
              historyIndex = null;
              break;
            default:
              term.write(
                `\r\nUnexpected syntax_check value: ${fut.syntax_check}`
              );
          }
        }

        term.onData(async (data) => {
          switch (data) {
            case "\r": // Enter
              term.write("\r\n");
              await execLine(buffer);
              buffer = "";
              cursorIndex = 0;
              term.write(prompt);
              break;
            case "\u0003": // Ctrl-C
              pyconsole.buffer.clear();
              buffer = "";
              cursorIndex = 0;
              term.write("^C\r\nKeyboardInterrupt\r\n" + ps1);
              prompt = ps1;
              historyIndex = null;
              break;
            case "\u0016": // Ctrl-V
              // paste from clipboard
              const clipboard = await navigator.clipboard.readText();
              const newBuf =
                buffer.slice(0, cursorIndex) +
                clipboard +
                buffer.slice(cursorIndex);
              setBuffer(newBuf, newBuf.length);
              break;
            case "\u007F": // Backspace
              if (cursorIndex > 0) {
                const before = buffer.slice(0, cursorIndex - 1);
                const after = buffer.slice(cursorIndex);
                cursorIndex -= 1;
                setBuffer(before + after, cursorIndex);
              }
              break;
            case "\x1B[A": // Up arrow
              if (prompt === ps1) {
                if (historyIndex === null) historyIndex = history.length;
                if (historyIndex > 0) {
                  historyIndex -= 1;
                  const newBuf = history[historyIndex] || "";
                  setBuffer(newBuf, newBuf.length);
                }
              }
              break;
            case "\x1B[B": // Down arrow
              if (prompt === ps1 && historyIndex !== null) {
                if (historyIndex < history.length - 1) {
                  historyIndex += 1;
                  const newBuf = history[historyIndex] || "";
                  setBuffer(newBuf, newBuf.length);
                } else {
                  historyIndex = null;
                  setBuffer("", 0);
                }
              }
              break;
            case "\x1B[C": // Right arrow
              if (cursorIndex < buffer.length) {
                cursorIndex += 1;
                refreshLine();
              }
              break;
            case "\x1B[D": // Left arrow
              if (cursorIndex > 0) {
                cursorIndex -= 1;
                refreshLine();
              }
              break;
            default:
              if (data) {
                // Normalize non-breaking spaces to regular spaces
                data = data.replace(/\u00a0/g, " ");
                // Insert arbitrary string at cursor position
                const before = buffer.slice(0, cursorIndex);
                const after = buffer.slice(cursorIndex);
                const newBuf = before + data + after;
                const newCursor = cursorIndex + data.length;
                setBuffer(newBuf, newCursor);
              }
          }
        });

        // 4. Extra features
        let idbkvPromise;
        async function getIDBKV() {
          if (!idbkvPromise) {
            idbkvPromise = await import(
              "https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js"
            );
          }
          return idbkvPromise;
        }

        async function mountDirectory(pyodideDirectory, directoryKey) {
          if (pyodide.FS.analyzePath(pyodideDirectory).exists) {
            return;
          }
          const { get, set } = await getIDBKV();
          const opts = { id: "mountdirid", mode: "readwrite" };
          let directoryHandle = await get(directoryKey);
          if (!directoryHandle) {
            directoryHandle = await showDirectoryPicker(opts);
            await set(directoryKey, directoryHandle);
          }
          const permissionStatus = await directoryHandle.requestPermission(
            opts
          );
          if (permissionStatus !== "granted") {
            throw new Error("readwrite access to directory not granted");
          }
          await pyodide.mountNativeFS(pyodideDirectory, directoryHandle);
        }
        globalThis.mountDirectory = mountDirectory;
      }
      window.console_ready = main();
    </script>
  </body>
</html>
